<?php

declare(strict_types=1);

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\Tests\Fixer\Phpdoc;

use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
use PhpCsFixer\Tests\Test\AbstractFixerTestCase;

/**
 * @internal
 *
 * @covers \PhpCsFixer\Fixer\Phpdoc\PhpdocOrderFixer
 *
 * @extends AbstractFixerTestCase<\PhpCsFixer\Fixer\Phpdoc\PhpdocOrderFixer>
 *
 * @author Graham Campbell <hello@gjcampbell.co.uk>
 * @author Jakub Kwaśniewski <jakub@zero-85.pl>
 *
 * @phpstan-import-type _AutogeneratedInputConfiguration from \PhpCsFixer\Fixer\Phpdoc\PhpdocOrderFixer
 *
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
 */
final class PhpdocOrderFixerTest extends AbstractFixerTestCase
{
    /**
     * @dataProvider provideInvalidConfigurationCases
     *
     * @param array<string, mixed> $configuration
     */
    public function testInvalidConfiguration(array $configuration, string $expectedExceptionMessage): void
    {
        self::expectException(InvalidFixerConfigurationException::class);
        self::expectExceptionMessage($expectedExceptionMessage);

        $this->fixer->configure($configuration);
    }

    /**
     * @return iterable<string, array{array<string, mixed>, string}>
     */
    public static function provideInvalidConfigurationCases(): iterable
    {
        yield 'empty order' => [
            ['order' => []],
            'The option "order" value is invalid. Minimum two tags are required.',
        ];

        yield 'invalid order' => [
            ['order' => ['param']],
            'The option "order" value is invalid. Minimum two tags are required.',
        ];

        yield 'duplicated tag' => [
            ['order' => ['param', 'return', 'throws', 'return']],
            'The option "order" value is invalid. Tag "return" is duplicated.',
        ];

        yield 'duplicated tags' => [
            ['order' => ['param', 'return', 'throws', 'param', 'return', 'throws']],
            'The option "order" value is invalid. Tags "param", "return" and "throws" are duplicated.',
        ];
    }

    /**
     * @param _AutogeneratedInputConfiguration $configuration
     *
     * @dataProvider provideFixCases
     */
    public function testFix(string $expected, ?string $input = null, array $configuration = []): void
    {
        $this->fixer->configure($configuration);
        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<string, array{0: string, 1?: ?string, 2?: _AutogeneratedInputConfiguration}>
     */
    public static function provideFixCases(): iterable
    {
        yield 'no changes' => [<<<'EOF'
            <?php
                /**
                 * Do some cool stuff.
                 *
                 * @param EngineInterface $templating
                 * @param string          $name
                 *
                 * @throws Exception
                 *
                 * @return void|bar
                 */

            EOF];

        yield 'only params 1' => [
            <<<'EOF'
                <?php
                    /**
                     * @param EngineInterface $templating
                     * @param string          $name
                     */

                EOF,
            null,
            ['order' => ['param', 'throw', 'return']],
        ];

        yield 'only params 2' => [
            <<<'EOF'
                <?php
                    /**
                     * @param EngineInterface $templating
                     * @param string          $name
                     */

                EOF,
            null,
            ['order' => ['param', 'return', 'throw']],
        ];

        yield 'only return 1' => [
            <<<'EOF'
                <?php
                    /**
                     *
                     * @return void|bar
                     *
                     */

                EOF,
            null,
            ['order' => ['param', 'throw', 'return']],
        ];

        yield 'only return 2' => [
            <<<'EOF'
                <?php
                    /**
                     *
                     * @return void|bar
                     *
                     */

                EOF,
            null,
            ['order' => ['param', 'return', 'throw']],
        ];

        yield 'empty 1' => [
            '/***/',
            null,
            ['order' => ['param', 'throw', 'return']],
        ];

        yield 'empty 2' => [
            '/***/',
            null,
            ['order' => ['param', 'return', 'throw']],
        ];

        yield 'no annotations 1' => [
            <<<'EOF'
                <?php
                    /**
                     *
                     *
                     *
                     */

                EOF,
            null,
            ['order' => ['param', 'throw', 'return']],
        ];

        yield 'no annotations 2' => [
            <<<'EOF'
                <?php
                    /**
                     *
                     *
                     *
                     */

                EOF,
            null,
            ['order' => ['param', 'return', 'throw']],
        ];

        yield 'basic case' => [
            <<<'EOF'
                <?php
                    /**
                     * @param string $foo
                     * @throws Exception
                     * @return bool
                     */

                EOF,
            <<<'EOF'
                <?php
                    /**
                     * @throws Exception
                     * @return bool
                     * @param string $foo
                     */

                EOF,
        ];

        yield 'complete case' => [
            <<<'EOF'
                <?php
                    /**
                     * Hello there!
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     *
                     *
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     *
                     *
                     *
                     * @param string $foo
                     * @param bool   $bar Bar
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     * @return bool Return false on failure.
                     * @return int  Return the number of changes.
                     */

                EOF,
            <<<'EOF'
                <?php
                    /**
                     * Hello there!
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     *
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     *
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     *
                     *
                     * @return bool Return false on failure.
                     * @return int  Return the number of changes.
                     *
                     * @param string $foo
                     * @param bool   $bar Bar
                     */

                EOF,
        ];

        yield 'example from Symfony' => [
            <<<'EOF'
                <?php
                    /**
                     * Renders a template.
                     *
                     * @param mixed $name       A template name
                     * @param array $parameters An array of parameters to pass to the template
                     *
                     *
                     * @throws \InvalidArgumentException if the template does not exist
                     * @throws \RuntimeException         if the template cannot be rendered
                     * @return string The evaluated template as a string
                     */

                EOF,
            <<<'EOF'
                <?php
                    /**
                     * Renders a template.
                     *
                     * @param mixed $name       A template name
                     * @param array $parameters An array of parameters to pass to the template
                     *
                     * @return string The evaluated template as a string
                     *
                     * @throws \InvalidArgumentException if the template does not exist
                     * @throws \RuntimeException         if the template cannot be rendered
                     */

                EOF,
        ];

        yield 'no changes with Laravel style' => [
            <<<'EOF'
                <?php
                    /**
                     * Do some cool stuff.
                     *
                     * @param EngineInterface $templating
                     * @param string          $name
                     *
                     * @return void|bar
                     *
                     * @throws Exception
                     */

                EOF,
            null,
            ['order' => ['param', 'return', 'throws']],
        ];

        yield 'basic case with Laravel style' => [
            <<<'EOF'
                <?php
                    /**
                     * @param string $foo
                     * @return bool
                     * @throws Exception
                     */

                EOF,
            <<<'EOF'
                <?php
                    /**
                     * @throws Exception
                     * @return bool
                     * @param string $foo
                     */

                EOF,
            ['order' => ['param', 'return', 'throws']],
        ];

        yield 'complete case with Laravel style' => [
            <<<'EOF'
                <?php
                    /**
                     * Hello there!
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     *
                     *
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     *
                     *
                     *
                     * @param string $foo
                     * @param bool   $bar Bar
                     * @return bool Return false on failure.
                     * @return int  Return the number of changes.
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     */

                EOF,
            <<<'EOF'
                <?php
                    /**
                     * Hello there!
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     *
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     *
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     *
                     *
                     * @return bool Return false on failure.
                     * @return int  Return the number of changes.
                     *
                     * @param string $foo
                     * @param bool   $bar Bar
                     */

                EOF,
            ['order' => ['param', 'return', 'throws']],
        ];

        yield 'example from Symfony with Laravel style' => [
            <<<'EOF'
                <?php
                    /**
                     * Renders a template.
                     *
                     * @param mixed $name       A template name
                     * @param array $parameters An array of parameters to pass to the template
                     *
                     * @return string The evaluated template as a string
                     *
                     * @throws \InvalidArgumentException if the template does not exist
                     * @throws \RuntimeException         if the template cannot be rendered
                     */

                EOF,
            null,
            ['order' => ['param', 'return', 'throws']],
        ];

        yield 'basic case with different order 1' => [
            <<<'EOF'
                <?php
                    /**
                     * @return bool
                     * @throws Exception
                     * @param string $foo
                     */

                EOF,
            <<<'EOF'
                <?php
                    /**
                     * @throws Exception
                     * @return bool
                     * @param string $foo
                     */

                EOF,
            ['order' => ['return', 'throws', 'param']],
        ];

        yield 'basic case with different order 2' => [
            <<<'EOF'
                <?php
                    /**
                     * @throws Exception
                     * @return bool
                     * @param string $foo
                     */

                EOF,
            null,
            ['order' => ['throws', 'return', 'param']],
        ];

        yield 'complete case with custom order' => [
            <<<'EOF'
                <?php
                    /**
                     * Hello there!
                     *
                     * Long description
                     * goes here.
                     *
                     *
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     *
                     *
                     *
                     * @return bool Return false on failure.
                     * @return int  Return the number of changes.
                     *
                     * @param string $foo
                     * @param bool   $bar Bar
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     * @internal
                     */

                EOF,
            <<<'EOF'
                <?php
                    /**
                     * Hello there!
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     *
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     *
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     *
                     *
                     * @return bool Return false on failure.
                     * @return int  Return the number of changes.
                     *
                     * @param string $foo
                     * @param bool   $bar Bar
                     */

                EOF,
            ['order' => [
                'throws',
                'return',
                'param',
                'custom',
                'internal',
            ]],
        ];

        yield 'intepacuthre' => [
            <<<'EOF'
                <?php
                    /**
                     * Hello there
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     * @template T of Extension\Extension
                     * @param string $foo
                     * @param bool   $bar Bar
                     * @param class-string<T> $id
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     * @return bool Return false on failure
                     * @return int  Return the number of changes.
                     **/

                EOF,
            <<<'EOF'
                <?php
                    /**
                     * Hello there
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     * @param string $foo
                     * @param bool   $bar Bar
                     * @param class-string<T> $id
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     * @template T of Extension\Extension
                     * @return bool Return false on failure
                     * @return int  Return the number of changes.
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     **/

                EOF,
            ['order' => ['internal', 'template', 'param', 'custom', 'throws', 'return']],
        ];

        yield 'pare' => [
            <<<'EOF'
                <?php
                    /**
                     * Hello there
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     * @template T of Extension\Extension
                     * @param string $foo
                     * @param bool   $bar Bar
                     * @param class-string<T> $id
                     * @return bool Return false on failure
                     * @return int  Return the number of changes.
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     **/

                EOF,
            <<<'EOF'
                <?php
                    /**
                     * Hello there
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     * @return bool Return false on failure
                     * @return int  Return the number of changes.
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     * @template T of Extension\Extension
                     * @param string $foo
                     * @param bool   $bar Bar
                     * @param class-string<T> $id
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     **/

                EOF,
            ['order' => ['param', 'return']],
        ];

        yield 'pareth' => [
            <<<'EOF'
                <?php
                    /**
                     * Hello there
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     * @template T of Extension\Extension
                     * @param string $foo
                     * @param bool   $bar Bar
                     * @param class-string<T> $id
                     * @return bool Return false on failure
                     * @return int  Return the number of changes.
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     **/

                EOF,
            <<<'EOF'
                <?php
                    /**
                     * Hello there
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     * @return bool Return false on failure
                     * @return int  Return the number of changes.
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     * @template T of Extension\Extension
                     * @param string $foo
                     * @param bool   $bar Bar
                     * @param class-string<T> $id
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     **/

                EOF,
            ['order' => ['param', 'return', 'throws']],
        ];

        yield 'pathre' => [
            <<<'EOF'
                <?php
                    /**
                     * Hello there
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     * @template T of Extension\Extension
                     * @param string $foo
                     * @param bool   $bar Bar
                     * @param class-string<T> $id
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     * @return bool Return false on failure
                     * @return int  Return the number of changes.
                     **/

                EOF,
            <<<'EOF'
                <?php
                    /**
                     * Hello there
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     * @return bool Return false on failure
                     * @return int  Return the number of changes.
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     * @template T of Extension\Extension
                     * @param string $foo
                     * @param bool   $bar Bar
                     * @param class-string<T> $id
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     **/

                EOF,
            ['order' => ['param', 'throws', 'return']],
        ];

        yield 'tepathre' => [
            <<<'EOF'
                <?php
                    /**
                     * Hello there
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     * @template T of Extension\Extension
                     * @param string $foo
                     * @param bool   $bar Bar
                     * @param class-string<T> $id
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     * @return bool Return false on failure
                     * @return int  Return the number of changes.
                     **/

                EOF,
            <<<'EOF'
                <?php
                    /**
                     * Hello there
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     * @return bool Return false on failure
                     * @return int  Return the number of changes.
                     * @param string $foo
                     * @param bool   $bar Bar
                     * @param class-string<T> $id
                     * @template T of Extension\Extension
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     **/

                EOF,
            ['order' => ['template', 'param', 'throws', 'return']],
        ];

        yield 'tepathre2' => [
            <<<'EOF'
                <?php
                    /**
                     * Hello there
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     * @template T of Extension\Extension
                     * @param string $foo
                     * @param bool   $bar Bar
                     * @param class-string<T> $id
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     * @return bool Return false on failure
                     * @return int  Return the number of changes.
                     **/

                EOF,
            <<<'EOF'
                <?php
                    /**
                     * Hello there
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     * @param string $foo
                     * @param bool   $bar Bar
                     * @param class-string<T> $id
                     * @return bool Return false on failure
                     * @return int  Return the number of changes.
                     * @template T of Extension\Extension
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     **/

                EOF,
            ['order' => ['template', 'param', 'throws', 'return']],
        ];

        yield 'multiline phpstan-return when phpstan order specified' => [
            <<<'EOF'
                    <?php
                        /**
                         * Returns next token if it's type equals to expected.
                         *
                         * @return array
                         * @phpstan-param TTokenType $type
                         * @phpstan-return (
                         *     $type is 'TableRow'
                         *         ? TTableRowToken
                         *         : TOtherToken
                         * )
                         * @throws ParserException
                         **/
                EOF,
            null,
            ['order' => ['param', 'return', 'phpstan-param', 'phpstan-return', 'throws']],
        ];

        yield 'multiple phpstan-param when phpstan order specified' => [
            <<<'EOF'
                    <?php
                        /**
                         * @phpstan-param non-empty-list<TGitHubReleaseArray> $releases
                         * @phpstan-param TCurrentVersionArray $currentVersion
                         * @phpstan-return TGitHubReleaseArray|false
                         */
                EOF,
            null,
            ['order' => ['param', 'return', 'phpstan-param', 'phpstan-return', 'throws']],
        ];

        yield 'mixed param, phpstan-param and psalm-param with order specified' => [
            <<<'EOF'
                    <?php
                        /**
                         * @param array $releases
                         * @param array $currentVersion
                         * @psalm-param list<array> $releases
                         * @psalm-param array<string> $releases
                         * @phpstan-param non-empty-list<TGitHubReleaseArray> $releases
                         * @phpstan-param TCurrentVersionArray $currentVersion
                         * @return array|false
                         * @phpstan-return TGitHubReleaseArray|false
                         */
                EOF,
            null,
            ['order' => ['param', 'psalm-param', 'phpstan-param', 'return']],
        ];

        yield 'phpstan- / psalm- annotations follow specified order for non-prefixed version by default' => [
            <<<'EOF'
                <?php
                    /**
                     * Hello there
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     * @phpstan-template T of Extension\Extension
                     * @param string $foo
                     * @param bool   $bar Bar
                     * @param string $id
                     * @phpstan-param TFooType $foo
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     * @phpstan-param class-string<T> $id
                     * @psalm-param 'foo'|'bar' $foo
                     * @psalm-param class-string<T> $id
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     * @return array Some array of results
                     * @phpstan-return TSomeResultArray
                     * @psalm-return array{
                     *    foo: string,
                     *    bar: ?int
                     * } Some array of results
                     **/

                EOF,
            <<<'EOF'
                <?php
                    /**
                     * Hello there
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     * @param string $foo
                     * @phpstan-return TSomeResultArray
                     * @return array Some array of results
                     * @param bool   $bar Bar
                     * @psalm-return array{
                     *    foo: string,
                     *    bar: ?int
                     * } Some array of results
                     * @param string $id
                     * @phpstan-template T of Extension\Extension
                     * @psalm-param 'foo'|'bar' $foo
                     * @phpstan-param TFooType $foo
                     * @psalm-param class-string<T> $id
                     * @custom Test!
                     *         asldnaksdkjasdasd
                     * @phpstan-param class-string<T> $id
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     **/

                EOF,
            ['order' => ['template', 'param', 'throws', 'return']],
        ];

        yield 'retains phpstan- / psalm- annotations that are not prefixed versions of configured order' => [
            <<<'EOF'
                <?php
                    /**
                     * Hello there
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     * @psalm-ignore-nullable-return
                     * @param string $id
                     * @phpstan-param class-string<T> $id
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     * @return array Some array of results
                     * @psalm-return TSomeResultArray
                     * @phpstan-pure
                     **/

                EOF,
            <<<'EOF'
                <?php
                    /**
                     * Hello there
                     *
                     * Long description
                     * goes here.
                     *
                     * @internal
                     * @psalm-ignore-nullable-return
                     * @return array Some array of results
                     * @phpstan-param class-string<T> $id
                     * @param string $id
                     * @psalm-return TSomeResultArray
                     * @throws Exception|RuntimeException dfsdf
                     *         jkaskdnaksdnkasndansdnansdajsdnkasd
                     * @phpstan-pure
                     **/

                EOF,
            ['order' => ['template', 'param', 'throws', 'return']],
        ];
    }
}
